package com.lsh.oms.core.records;

import com.alibaba.fastjson.JSON;
import com.google.common.collect.Sets;
import com.lsh.base.http.client.ResponseAssertions;
import com.lsh.base.http.client.core.HttpClient;
import com.lsh.base.lang.*;
import com.lsh.base.lang.json.Jsons;
import com.lsh.oms.core.dao.other.ReturnRequestMapper;
import com.lsh.oms.core.exception.BusinessException;
import com.lsh.oms.core.model.order.*;
import com.lsh.oms.core.records.core.Return;
import com.lsh.oms.core.records.core.ReturnRequest;
import com.lsh.oms.core.redis.RedisTemplate;
import com.lsh.oms.core.service.order.OrderQueryService;
import com.lsh.oms.core.service.shipping.core.ShippingService;
import com.lsh.oms.core.service.sign.core.SignService;
import com.lsh.oms.core.service.tool.Criteria;
import com.lsh.oms.core.service.tool.Criterias;
import com.lsh.oms.core.service.tool.Pageable;
import com.lsh.oms.core.service.tool.Summaries;
import com.lsh.oms.core.utils.BatchNumberGenerator;
import com.lsh.oms.core.utils.IdGenerator;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.lsh.base.lang.BigDecimals.divideWithScale4;
import static com.lsh.base.lang.BigDecimals.greaterThanZero;
import static com.lsh.base.lang.bean.Beans.*;
import static com.lsh.oms.core.records.core.ReturnRequest.Occasion.AFTER_SIGN;
import static com.lsh.oms.core.records.core.ReturnRequest.Occasion.BEFORE_SIGN;
import static java.util.stream.Collectors.*;

@Component
public class ReturnRequestRepository implements com.lsh.oms.core.records.core.ReturnRequestRepository {

    private static final Logger LOGGER = LoggerFactory.getLogger(ReturnRequestRepository.class);

    @Resource
    private OrderQueryService orders;

    @Resource
    private SignService signs;

    @Resource
    private ShippingService shippings;

    @Resource
    private ReturnRepository returns;

    @Resource
    private ReturnRequestMapper mapper;

    @Resource(name = "apache.http.client")
    private HttpClient https;

    @Resource
    private RedisTemplate redis;

    @Value("${days.of.returnable.period:30}")
    private Integer daysOfReturnablePeriod;

    @Value("${signCalUrl}")
    private String calculatorServiceBaseUri;

    @Value("${mis.host}")
    private String misServiceBaseUri;

    @Resource
    private BatchNumberGenerator numberGenerator;

    private static final Set<String> USER_CONCERNED = Sets.newHashSet(
            "uid", "zone_id", "address_id", "address", "contact_name",
            "contact_phone", "market_name", "username", "user_type",
            "province", "city", "county", "area",
            "province_name", "city_name", "county_name", "area_name");

    @Transactional(rollbackFor = Exception.class)
    @Override
    public ReturnRequest create(Map<String, Object> request) {
        LOGGER.info("create return request, request: {}", request);
        OrderHead order = findOrder(request);
        OrderShippingHeadWithBLOBs shipping = findShippingOrder(order);
        OrderSignHead receipt = findSignOrder(order);
        ensureReturnable(request, order, shipping, receipt);
        enhance(request, order, shipping, receipt);
        mapper.insert(request, shipping, receipt);
        mapper.insertDetails(request);
        ReturnRequest created = find(getLong(request, "id")).orElseThrow(RuntimeException::new);
        created.start(request);
        return created;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void createBatch(Map<String, Object> request) {
        getOrders(request).forEach($ -> {
            OrderHead order = orders.findById($, false, true).orElseThrow(BusinessException::orderNotFound);
            OrderSignHead sign = signs.findByOrderId($).orElseThrow(BusinessException::receiptNotFound);
            List<Map<String, Object>> returnItems = buildReturnItems(order, sign);
            if (Lists.isNotEmpty(returnItems)) {
                ReturnRequest returnRequest = create(Maps.of("occasion", AFTER_SIGN.name(), "party", Maps.of("username", "批量创建", "type", "ADMIN"), "order", Maps.newHashMap("id", $), "details", returnItems));
                Long returnId = ((ReturnRequestRecord) returnRequest).returnId;
                Return return_ = returns.find(returnId).orElseThrow(BusinessException::returnNotFound);
                preDeliver(return_, request);
                deliverAndCheck(return_, request);
            } else {
                LOGGER.warn("order: {} does not sign any thing", $);
            }
        });
    }

    private void preDeliver(Return return_, Map<String, Object> request) {
        Optional.ofNullable(getString(request, "route_id")).ifPresent($ -> return_.preDeliver(Maps.of("route_id", $)));
    }

    private void deliverAndCheck(Return return_, Map<String, Object> request) {
        Optional.ofNullable(getLong(request, "deliveryman_id")).ifPresent($ -> {
            return_.deliver(Maps.of("deliveryman_id", $));
            return_.confirm(Maps.empty());
        });
    }

    private List<Map<String, Object>> buildReturnItems(OrderHead order, OrderSignHead sign) {
        Set<Integer> weighingGoods = weighingGoods(order);
        return sign.getDetails().stream().map($ -> {
            BigDecimal signQuantity = weighingGoods.contains($.getItemCode()) ? $.getRealQty() : $.getQty();
            if (greaterThanZero(signQuantity)) {
                return Maps.of(
                        "item_code", $.getItemCode(),
                        "quantity", signQuantity,
                        "reason", Maps.of("code", "SHORTCUT", "notes", "", "images", Collections.emptyList()),
                        "opinion", Maps.of("approved", true, "discarded", false, "notes", "")
                );
            } else {
                return Maps.empty();
            }
        }).filter($ -> !$.isEmpty()).collect(Collectors.toList());
    }

    private Set<Integer> weighingGoods(OrderHead order) {
        return order.getDetails().stream().filter($ -> $.isWeighingGoods()).map($ -> $.getItemCode()).collect(Collectors.toSet());
    }

    private List<Long> getOrders(Map<String, Object> request) {
        List<Long> orders = get(request, "orders");
        if (Lists.isEmpty(orders)) {
            throw BusinessException.badRequest("订单号不能为空");
        }
        return orders;
    }

    @Override
    public Pageable query(Criteria criteria, long page, long pageLimit) {
        Long count = mapper.countBaseOnCriteria(criteria);
        List<ReturnRequestRecord> result = count > 0
                ? mapper.findBaseOnCriteria(criteria, Pageable.start(page, pageLimit), pageLimit)
                : Collections.emptyList();
        fillDetails(criteria, result);
        return new Pageable<>(result, page, pageLimit, count);
    }

    private void fillDetails(Criteria criteria, List<ReturnRequestRecord> records) {
        if (Criterias.detailsRequired(criteria)) {
            Map<Long, List<ReturnRequestRecord.ReturnRequestDetailRecord>> group = findDetails(records.stream().map($ -> $.id).collect(toList()))
                    .stream().collect(groupingBy($ -> $.returnRequestId));
            records.forEach($ -> {
                $.details = group.get($.id());
                $.details.forEach(d -> d.returnRequest = $);
            });
        }
    }

    private void enhance(Map<String, Object> request, OrderHead order, OrderShippingHeadWithBLOBs shipping, OrderSignHead receipt) {
        List<Map<String, Object>> returnDetails = returnDetails(request);
        request.put("id", IdGenerator.genId());

        request.put("ext", Maps.newHashMap(
                "user", userInfo(order),
                "order", orderExt(request, order),
                "storage_type", shipping.getStorageTypeId() == 1 ? "NORMAL" : "COLD",
                "receipt", receiptExt(request, receipt),
                "shipping", shippingExt(request, shipping)));

        request.put("occasion", getString(request, "occasion"));
        String shippingDeliveryWay = Integer.valueOf(4).equals(getInt(shipping.getWarehouseInfo(), "delivery_type")) ? "DIRECT" : "TMS";
        request.put("shipping_delivery_way", shippingDeliveryWay);
        boolean checkImmediately = ifPresent(returnDetails.get(0), "opinion");
        request.put("check_immediately", checkImmediately);
        request.put("number", checkImmediately ? null : numberGenerator.generateReturnRequestNumber(order.getOrderCode()));

        Map<Integer, OrderShippingDetail> shippingDetails = shipping.getDetails().stream().collect(toMap($ -> $.getItemCode(), $ -> $));

        Set<Integer> weighingGoods = order.getDetails().stream().filter($ -> $.isWeighingGoods()).map($ -> $.getItemCode()).collect(Collectors.toSet());
        LOGGER.info("those items are weighing goods: {}", Jsons.toJsonString(weighingGoods));

        Map<Integer, OrderSignDetail> receiptDetails = Optional.ofNullable(receipt)
                .map(r -> r.getDetails().stream().collect(toMap($ -> $.getItemCode(), $ -> $)))
                .orElse(Collections.emptyMap());

        returnDetails.forEach(item -> {
            Integer itemCode = getInt(item, "item_code");
            Map<String, Object> ext = new HashMap<>();
            Optional.ofNullable(shippingDetails.get(itemCode)).ifPresent($ -> {
                item.put("id", IdGenerator.genId());

                ext.put("goods", Maps.newHashMap("item_id", $.getItemId(), "code", $.getCode(),
                        "images", JSON.parseArray(getString($.getExtendedProperties(), "img_list"), String.class)));

                ext.put("shipping_detail", Maps.newHashMap("images", JSON.parseArray(getString($.getExtendedProperties(), "img_list"), String.class)));

                Optional.ofNullable(receiptDetails.get(itemCode)).ifPresent(rd
                        -> ext.put("receipt_detail", Maps.newHashMap("images", JSON.parseArray(getString(rd.getExtendedProperties(), "img_list"), String.class))));

                item.put("ext", ext);
                item.put("sku_id", $.getSkuId());
                item.put("sku_name", $.getSkuName());
                item.put("is_weighing_goods", weighingGoods.contains($.getItemCode()));
                item.put("sale_unit", $.getSaleUnit());
            });
        });

        Map<Integer, BigDecimal> eachPrice = getItemPriceFromCalService(returnDetails, shipping);
        returnDetails.forEach($ -> $.put("price", eachPrice.get(getInt($, "item_code"))));
        request.put("details", returnDetails);
    }

    private Map<String, Object> orderExt(Map<String, Object> request, OrderHead order) {
        Map<String, Object> orderExt = Maps.of("ordered_at", order.getOrderTime(), "pay_type", order.getPayType());
        Optional.ofNullable(getMap(request, "order")).ifPresent(orderExt::putAll);
        return orderExt;
    }

    private Map<String, Object> shippingExt(Map<String, Object> request, OrderShippingHeadWithBLOBs shipping) {
        Map<String, Object> shippingExt = Maps.of("id", shipping.getShippingOrderId(), "route_id", shipping.getRouteId(), "deliveryman_id", shipping.getTransUid(), "arrived_at", shipping.getActivedAt());
        Optional.ofNullable(getMap(request, "shipping")).ifPresent(shippingExt::putAll);
        shippingExt.putIfAbsent("deny", false);
        return shippingExt;
    }

    private Map<String, Object> receiptExt(Map<String, Object> request, OrderSignHead receipt) {
        Map<String, Object> receiptExt = (receipt != null) ? Maps.newHashMap("id", receipt.getReceiptOrderId()) : Maps.newHashMap();
        Optional.ofNullable(getMap(request, "receipt")).ifPresent(receiptExt::putAll);
        return receiptExt;
    }

    private Map<Integer, BigDecimal> getItemPriceFromCalService(List<Map<String, Object>> returnDetails, OrderShippingHeadWithBLOBs shipping) {

        List<Map<String, ? extends Number>> skuList = returnDetails.stream()
                .map($ -> Maps.newHashMap("sku_id", getLong($, "sku_id"),
                        "item_code", getInt($, "item_code"),
                        "qty", getBoolean($, "is_weighing_goods") ? divideWithScale4(getBigDecimal($, "quantity"), getBigDecimal($, "sale_unit")) : getBigDecimal($, "quantity")))
                .collect(toList());

        List<Map<String, Object>> details = shipping.getDetails().stream().map($ -> {
            Map<String, Object> properties = $.getExtendedProperties();
            properties.put("item_code", $.getItemCode());
            properties.put("sku_id", $.getSkuId().toString());
            properties.put("sku_name", $.getSkuName());
            properties.put("sale_unit", $.getSaleUnit().toString());
            properties.put("real_qty", $.getRealQty());
            properties.put("qty", $.getQty());
            properties.put("money", $.getMoney().toString());
            properties.put("price", $.getPrice().toString());
            return properties;
        }).collect(toList());

        Map<String, Object> shippingExtendedProperties = shipping.getExtendedProperties();

        Map<String, Object> request = Maps.newHashMap(
                "order_id", shipping.getOrderId().toString(),
                "sku_list", Jsons.toJsonString(skuList),
                "shipping_info", Jsons.toJsonString(Maps.newHashMap("head_info", getMap(shippingExtendedProperties, "head_info"), "detail_list", details)));
        Map<String, Object> response = https.postFrom(calculatorServiceBaseUri + "/cal/index/return", request);
        if (!ResponseAssertions.success(response)) {
            throw BusinessException.badRequest("退货商品单价获取失败");
        }
        Map<Integer, BigDecimal> result = new HashMap<>();
        getList(response, "content.return.detail").forEach($ -> {
            Integer itemCode = getInt($, "item_code");
            BigDecimal price = Optional.ofNullable(getBigDecimal($, "price")).orElseThrow(() -> BusinessException.badRequest("商品" + itemCode + "单价获取失败"));
            result.put(itemCode, price);
        });
        return result;
    }

    private List<Map<String, Object>> returnDetails(Map<String, Object> request) {
        List<Map<String, Object>> returnDetails = get(request, "details");
        if (returnDetails == null || returnDetails.isEmpty()) {
            throw BusinessException.badRequest("退货项缺失");
        }
        return returnDetails;
    }

    private OrderHead findOrder(Map<String, Object> request) {
        OrderHead order = this.orders.findByCode(getLong(request, "order.id"), false, true);
        if (order == null) {
            throw BusinessException.orderNotFound();
        }
        return order;
    }

    private OrderSignHead findSignOrder(OrderHead order) {
        return signs.findByOrderId(order.getOrderCode()).orElse(null);
    }

    private OrderShippingHeadWithBLOBs findShippingOrder(OrderHead order) {
        return shippings.findByOrderId(order.getOrderCode(), true).orElseThrow(BusinessException::shippingNotFound);
    }

    private Map<String, Object> userInfo(OrderHead order) {
        Map<String, Object> rawUserExt = Jsons.fromJsonString(order.getAddressInfo());
        Map<String, Object> userExt = rawUserExt.entrySet().stream().filter($ -> USER_CONCERNED.contains($.getKey())).collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
        Optional.ofNullable(rawUserExt.get("position")).map($ -> Jsons.fromJsonString($.toString())).ifPresent(userExt::putAll);
        return userExt;
    }

    private void ensureReturnable(Map<String, Object> request, OrderHead order, OrderShippingHead shipping, OrderSignHead receipt) {
        ensureOrderPaid(request, order, receipt);
        ensureAfterSaleReturnAllowable(request, receipt);
        ensureNotOverPeriod(receipt);
        ensureQuantityNotOver(request, order, shipping, receipt);
        ensureSpotReturnNotDuplicate(request, order);
    }

    private void ensureOrderPaid(Map<String, Object> request, OrderHead order, OrderSignHead receipt) {
        boolean paid = order.paid() || (receipt != null && receipt.paid());
        if (afterSaleReturn(request) && !paid) {
            throw BusinessException.badRequest("订单还未支付");
        }
    }

    private void ensureSpotReturnNotDuplicate(Map<String, Object> request, OrderHead order) {
        if (spotReturn(request)) {
            if (mapper.countBaseOnCriteria(Criteria.of(
                    "order_id", order.getOrderCode().toString(),
                    "occasion", BEFORE_SIGN.name(),
                    "check_immediately", "all")) > 0) {
                throw BusinessException.badRequest("重复的退货申请");
            }
        }
    }

    private void ensureAfterSaleReturnAllowable(Map<String, Object> request, OrderSignHead receipt) {
        Optional.ofNullable(getString(request, "occasion")).orElseThrow(BusinessException::badRequest);
        if (receipt == null && afterSaleReturn(request)) {
            throw BusinessException.badRequest("全单拒签订单不允许售后申请");
        }
        if (afterSaleReturn(request) && hasReturnBefore(receipt)) {
            throw BusinessException.badRequest("不支持二次退货");
        }
    }

    /**
     * for archived data
     */
    private boolean hasReturnBefore(OrderSignHead receipt) {
        String returnThreshold = redis.get("return.threshold");
        if (returnThreshold != null) {
            DateTime threshold = DateTimes.of(Numbers.toInt(returnThreshold));
            DateTime signedAt = DateTimes.of(receipt.getCreatedAt());
            LOGGER.info("return threshold: {}, receipt created at: {}", threshold, signedAt);
            if (signedAt.isBefore(threshold)) {
                Map<String, Object> response = https.get(misServiceBaseUri + "/order/afs/getmain", Maps.of("venderId", receipt.getVenderId()), Maps.of("order_id", receipt.getOrderId()));
                if (!ResponseAssertions.success(response)) {
                    throw BusinessException.badRequest("售后查询失败");
                }
                return "80".equals(getString(response, "content.status"));
            }
        }
        return false;
    }

    private void ensureQuantityNotOver(Map<String, Object> request, OrderHead order, OrderShippingHead shipping, OrderSignHead receipt) {
        // Integer -> item_code, BigDecimal -> quantity

        Set<Integer> weighingGoods = order.getDetails().stream().filter($ -> $.isWeighingGoods()).map($ -> $.getItemCode()).collect(Collectors.toSet());

        Map<Integer, BigDecimal> total = afterSaleReturn(request)
                ? receipt.getDetails().stream().collect(Collectors.groupingBy(OrderSignDetail::getItemCode,
                Collectors.reducing(BigDecimal.ZERO, $ -> weighingGoods.contains($.getItemCode()) ? $.getRealQty() : $.getQty(), BigDecimal::add)))
                : shipping.getDetails().stream().collect(Collectors.groupingBy(OrderShippingDetail::getItemCode,
                Collectors.reducing(BigDecimal.ZERO, $ -> weighingGoods.contains($.getItemCode()) ? $.getRealQty() : $.getQty(), BigDecimal::add)));

        Map<Integer, BigDecimal> applied = mapper.totalApplied(shipping.getOrderId()).stream()
                .collect(Collectors.groupingBy($ -> $.itemCode,
                        Collectors.reducing(BigDecimal.ZERO, $ -> $.deliveredQuantity(), BigDecimal::add)));

        Map<Integer, BigDecimal> applying = ((List<Map<String, Object>>) get(request, "details")).stream()
                .collect(Collectors.groupingBy($ -> getInt($, "item_code"),
                        Collectors.reducing(BigDecimal.ZERO, $ -> getBigDecimal($, "quantity"), BigDecimal::add)));

        applying.forEach((key, value) -> {
            BigDecimal applicableQuantity = total.get(key);
            BigDecimal appliedQuantity = Optional.ofNullable(applied.get(key)).orElse(BigDecimal.ZERO);
            BigDecimal applyingQuantity = applying.get(key);
            if (BigDecimals.greaterThan(applyingQuantity, applicableQuantity.subtract(appliedQuantity))) {
                throw new BusinessException("quantity_request_over", "行项目: " + key + "退货数超出");
            }
        });
    }

    private boolean spotReturn(Map<String, Object> request) {
        return BEFORE_SIGN.name().equals(getString(request, "occasion"));
    }

    private boolean afterSaleReturn(Map<String, Object> request) {
        return AFTER_SIGN.name().equals(getString(request, "occasion"));
    }

    private void ensureNotOverPeriod(OrderSignHead receipt) {
        Optional.ofNullable(receipt).ifPresent($ -> {
            DateTime signedAt = DateTimes.of($.getCreatedAt());
            if (DateTimes.now().minusDays(daysOfReturnablePeriod()).isAfter(signedAt)) {
                throw new BusinessException("request_over_returnable_period", "超出退货时间天数限制: " + daysOfReturnablePeriod);
            }
        });
    }

    private int daysOfReturnablePeriod() {
        Integer daysOfReturnablePeriod = Optional.ofNullable(redis.get("days.of.returnable.period")).map(Numbers::toInt).orElse(this.daysOfReturnablePeriod);
        LOGGER.info("days of returnable period: {}", daysOfReturnablePeriod);
        return daysOfReturnablePeriod;
    }

    @Override
    public Optional<ReturnRequest> find(Long id, boolean detailsRequired) {
        ReturnRequestRecord record = mapper.findById(id);
        if (record == null) {
            return Optional.empty();
        }
        if (detailsRequired) {
            record.details = findDetails(Lists.newArrayList(id));
            record.details.forEach($ -> $.returnRequest = record);
        }
        return Optional.of(record);
    }

    @Override
    public Optional<ReturnRequest> find(Long id) {
        return find(id, true);
    }

    @Override
    public Map<String, Long> summary(Criteria criteria) {
        criteria.remove("status");
        Map<String, Long> result = Summaries.enhance(mapper.groupByStatus(criteria));
        Stream.of(ReturnRequest.Status.values()).forEach($ -> {
            String key = Summaries.key($.name());
            if (!result.containsKey(key)) {
                result.put(key, 0L);
            }
        });
        return result;
    }

    private List<ReturnRequestRecord.ReturnRequestDetailRecord> findDetails(Collection<Long> returnIds) {
        return returnIds.isEmpty() ? Collections.emptyList() : mapper.findDetails(returnIds);
    }
}
